Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/next/pages/news/[id]/[timestamp].tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2023 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Alert, Breadcrumb, Col, Layout, Radio, Row } from "antd";67import { GetServerSidePropsContext } from "next";8import { useRouter } from "next/router";9import TimeAgo from "timeago-react";1011import { getNewsItemUser } from "@cocalc/database/postgres/news";12import { Icon } from "@cocalc/frontend/components/icon";13import { slugURL } from "@cocalc/util/news";14import Footer from "components/landing/footer";15import Head from "components/landing/head";16import Header from "components/landing/header";17import A from "components/misc/A";18import { News } from "components/news/news";19import { NewsWithFuture } from "components/news/types";20import { useDateStr } from "components/news/useDateStr";21import { MAX_WIDTH, NOT_FOUND } from "lib/config";22import { Customize, CustomizeType } from "lib/customize";23import useProfile from "lib/hooks/profile";24import { extractID } from "lib/news";25import withCustomize from "lib/with-customize";2627interface Props {28customize: CustomizeType;29news: NewsWithFuture;30timestamp: number; // unix epoch in seconds31prev?: number;32next?: number;33}3435export default function NewsPage(props: Props) {36const { customize, news, timestamp, prev, next } = props;37const { siteName } = customize;38const router = useRouter();39const profile = useProfile({ noCache: true });40const isAdmin = profile?.is_admin;41const permalink = slugURL(news);42const dateStr = useDateStr(news, true);4344const { id } = news;45const title = `${news.title}@${dateStr} – News – ${siteName}`;4647function future() {48if (news.future && !isAdmin) {49return (50<Alert type="info" banner={true} message="News not yet published" />51);52}53}5455function content() {56if (isAdmin || !news.future) {57return <News news={news} showEdit={isAdmin} historyMode standalone />;58}59}6061function breadcrumb() {62const items = [63{ key: "/", title: <A href="/">{siteName}</A> },64{ key: "/news", title: <A href="/news">News</A> },65{ key: "permalink", title: <A href={permalink}>#{news.id}</A> },66{67key: "timestamp",68title: (69<A href={`/news/${news.id}/${timestamp}`}>70<TimeAgo datetime={1000 * timestamp} />71</A>72),73},74];75return <Breadcrumb items={items} />;76}7778function up() {79return (80<Radio.Group buttonStyle="outline" size="small">81<Radio.Button82disabled={!prev}83style={{ userSelect: "none" }}84onClick={() => {85prev && router.push(`/news/${id}/${prev}`);86}}87>88<Icon name="arrow-left" /> Older89</Radio.Button>90<Radio.Button91style={{ userSelect: "none" }}92onClick={() => {93router.push(slugURL(news));94}}95>96<Icon name="arrow-up" /> Current97</Radio.Button>98<Radio.Button99disabled={!next}100style={{ userSelect: "none" }}101onClick={() => {102next && router.push(`/news/${id}/${next}`);103}}104>105<Icon name="arrow-right" /> Newer106</Radio.Button>107</Radio.Group>108);109}110111function renderTop() {112return (113<Row justify="space-between" gutter={15} style={{ margin: "30px 0" }}>114<Col>{breadcrumb()}</Col>115<Col>{up()}</Col>116</Row>117);118}119120return (121<Customize value={customize}>122<Head title={title} />123<Layout>124<Header />125<Layout.Content126style={{127backgroundColor: "white",128}}129>130<div131style={{132minHeight: "75vh",133maxWidth: MAX_WIDTH,134padding: "30px 15px",135margin: "0 auto",136}}137>138{renderTop()}139{future()}140{content()}141</div>142<Footer />143</Layout.Content>144</Layout>145</Customize>146);147}148149export async function getServerSideProps(context: GetServerSidePropsContext) {150const { query } = context;151152const id = extractID(query.id);153if (id == null) return NOT_FOUND;154155// we just re-use the logic for the id156const timestamp = extractID(query.timestamp);157if (timestamp == null) return NOT_FOUND;158159try {160const news = await getNewsItemUser(id);161if (news == null) {162throw new Error(`not found`);163}164165const { history } = news;166167if (history == null) return NOT_FOUND;168169const historic = history[timestamp];170if (historic == null) {171throw new Error(`history ${timestamp} not found`);172}173174// sort keys in news.history by their timestamp value175const timestamps = Object.keys(history)176.map((ts) => Number(ts))177.filter((ts) => !Number.isNaN(ts))178.sort((a, b) => a - b);179// prev and next are the timestamps of the previous and next news item180const prev = timestamps[timestamps.indexOf(timestamp) - 1] ?? null;181const next = timestamps[timestamps.indexOf(timestamp) + 1] ?? null;182183return await withCustomize({184context,185props: {186timestamp,187prev,188next,189news: { ...news, ...historic, date: timestamp },190},191});192} catch (err) {193console.warn(`Error getting news with id=${id}`, err);194}195196return NOT_FOUND;197}198199200